Skip to content

Add CrosschainRemoteExecutor and GovernorCrosschain#6272

Merged
Amxx merged 19 commits intoOpenZeppelin:masterfrom
Amxx:crosschain/remote-executor-controller
Mar 4, 2026
Merged

Add CrosschainRemoteExecutor and GovernorCrosschain#6272
Amxx merged 19 commits intoOpenZeppelin:masterfrom
Amxx:crosschain/remote-executor-controller

Conversation

@Amxx
Copy link
Collaborator

@Amxx Amxx commented Jan 9, 2026

Fixes #5903

PR Checklist

  • Tests
  • Documentation
  • Changeset entry (run npx changeset add)

@Amxx Amxx requested a review from a team as a code owner January 9, 2026 13:17
@changeset-bot
Copy link

changeset-bot bot commented Jan 9, 2026

🦋 Changeset detected

Latest commit: 1ed3b79

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
openzeppelin-solidity Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 9, 2026

Walkthrough

This pull request introduces two new abstract smart contracts for cross-chain communication: CrosschainRemoteController manages cross-chain linkage by storing gateway and counterpart addresses per chain and dispatching cross-chain messages through registered gateways, while CrosschainRemoteExecutor extends ERC7786Recipient to receive and process incoming cross-chain messages by decoding mode-encoded payloads and executing single calls, batch calls, or delegatecalls. A mock contract and comprehensive test suite are also added to support development and validation.

Possibly related PRs

  • PR #5914: Implements ERC-7786 cross-chain link management with similar _setLink and link storage patterns alongside gateway messaging logic
  • PR #5904: Integrates ERC-7786 primitives (ERC7786Recipient, IERC7786GatewaySource) that are directly extended and used by the controller and executor contracts

Suggested labels

crosschain

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ⚠️ Warning The pull request title mentions 'GovernorCrosschain' but the changeset only introduces CrosschainRemoteExecutor and CrosschainRemoteController; GovernorCrosschain is not present in any of the modified files. Update the title to accurately reflect the actual changes: 'Add CrosschainRemoteExecutor and CrosschainRemoteController' or similar.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR introduces CrosschainRemoteExecutor and CrosschainRemoteController contracts that implement ERC7786 RemoteExecutor functionality as required by issue #5903.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the crosschain remote executor and controller contracts with supporting tests and mocks.
Description check ✅ Passed The PR description references issue #5903 and indicates completion of tests and documentation, which relates to the changeset introducing CrosschainRemoteExecutor and CrosschainRemoteController contracts.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @contracts/crosschain/CrosschainRemoteController.sol:
- Around line 26-33: Add an explicit validation in _crosschainExecute: call
getLink(chain) as before, then require that gateway != address(0) (and
optionally counterpart.length > 0) and revert with a descriptive error such as
LinkNotRegistered(chain) (from CrosschainLinked) before calling
IERC7786GatewaySource(gateway).sendMessage; this prevents calling the zero
address and surfaces a clear error when a chain link is unregistered.
🧹 Nitpick comments (1)
test/crosschain/CrosschainExecutor.test.js (1)

131-150: Consider specifying the expected revert reason for direct setter call.

The crosschain call path correctly expects FailedCall, but the direct call (line 133) uses a bare .to.be.reverted without specifying the expected error. This could mask unexpected revert reasons.

♻️ Suggested improvement
-      await expect(this.executor.$_setup(this.eoa.address, this.chain.toErc7930(this.newController))).to.be.reverted;
+      await expect(this.executor.$_setup(this.eoa.address, this.chain.toErc7930(this.newController)))
+        .to.be.revertedWithoutReason(); // EOA has no code, so supportsAttribute call fails

Or if there's a specific custom error expected, use revertedWithCustomError.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad83688 and d3dbb62.

📒 Files selected for processing (4)
  • contracts/crosschain/CrosschainRemoteController.sol
  • contracts/crosschain/CrosschainRemoteExecutor.sol
  • contracts/mocks/crosschain/CrosschainRemoteControllerMock.sol
  • test/crosschain/CrosschainExecutor.test.js
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: Amxx
Repo: OpenZeppelin/openzeppelin-contracts PR: 5914
File: contracts/crosschain/bridges/BridgeERC20.sol:57-58
Timestamp: 2025-10-03T13:14:57.679Z
Learning: In cross-chain bridge contracts like BridgeERC20, when processing incoming messages in _processMessage, avoid validation checks that revert on malformed addresses. Reverting would create cross-chain inconsistency where tokens are locked/burned on the source chain but never minted on the destination. Instead, use best-effort address extraction (e.g., address(bytes20(toBinary))) to maintain atomicity across chains. If tokens are minted to an incorrect address due to user error, recovery may be possible through admin controls rather than leaving funds permanently locked.
📚 Learning: 2025-10-03T13:14:57.679Z
Learnt from: Amxx
Repo: OpenZeppelin/openzeppelin-contracts PR: 5914
File: contracts/crosschain/bridges/BridgeERC20.sol:57-58
Timestamp: 2025-10-03T13:14:57.679Z
Learning: In cross-chain bridge contracts like BridgeERC20, when processing incoming messages in _processMessage, avoid validation checks that revert on malformed addresses. Reverting would create cross-chain inconsistency where tokens are locked/burned on the source chain but never minted on the destination. Instead, use best-effort address extraction (e.g., address(bytes20(toBinary))) to maintain atomicity across chains. If tokens are minted to an incorrect address due to user error, recovery may be possible through admin controls rather than leaving funds permanently locked.

Applied to files:

  • contracts/crosschain/CrosschainRemoteController.sol
  • contracts/crosschain/CrosschainRemoteExecutor.sol
📚 Learning: 2025-08-29T13:16:08.640Z
Learnt from: Amxx
Repo: OpenZeppelin/openzeppelin-contracts PR: 5904
File: contracts/mocks/crosschain/ERC7786RecipientMock.sol:12-14
Timestamp: 2025-08-29T13:16:08.640Z
Learning: In OpenZeppelin contracts, mock contracts (like ERC7786RecipientMock) don't require input validation such as zero-address checks in constructors, as they are only used for testing purposes in controlled environments.

Applied to files:

  • contracts/mocks/crosschain/CrosschainRemoteControllerMock.sol
  • contracts/crosschain/CrosschainRemoteExecutor.sol
📚 Learning: 2025-08-28T16:58:18.879Z
Learnt from: Amxx
Repo: OpenZeppelin/openzeppelin-contracts PR: 5904
File: contracts/mocks/crosschain/ERC7786GatewayMock.sol:27-31
Timestamp: 2025-08-28T16:58:18.879Z
Learning: In the OpenZeppelin ERC7786 implementation, the `UnsupportedAttribute(bytes4 selector)` error is defined in the `IERC7786GatewaySource` interface, making it directly accessible to inheriting contracts without needing interface qualification.

Applied to files:

  • contracts/crosschain/CrosschainRemoteExecutor.sol
🧬 Code graph analysis (1)
test/crosschain/CrosschainExecutor.test.js (2)
test/crosschain/ERC7786Recipient.test.js (1)
  • getLocalChain (15-15)
test/helpers/erc7579.js (6)
  • encodeMode (15-24)
  • encodeSingle (26-27)
  • CALL_TYPE_BATCH (12-12)
  • encodeBatch (29-39)
  • CALL_TYPE_DELEGATE (13-13)
  • encodeDelegate (41-42)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Redirect rules - solidity-contracts
  • GitHub Check: Header rules - solidity-contracts
  • GitHub Check: Pages changed - solidity-contracts
  • GitHub Check: tests
  • GitHub Check: slither
  • GitHub Check: tests-foundry
  • GitHub Check: halmos
  • GitHub Check: tests-upgradeable
  • GitHub Check: coverage
🔇 Additional comments (13)
contracts/mocks/crosschain/CrosschainRemoteControllerMock.sol (1)

8-10: LGTM!

Simple mock contract that correctly extends CrosschainRemoteController and forwards the constructor parameters. Appropriate for testing purposes. Based on learnings, mock contracts don't require additional input validation.

test/crosschain/CrosschainExecutor.test.js (4)

16-35: LGTM!

The fixture correctly establishes the bidirectional link: the executor knows about the controller (via constructor), and the controller knows about the executor (via $_setLink). Event verification during setup is good practice.


42-52: LGTM!

Good verification of the bidirectional relationship between controller and executor. The use of eventually.deep.equal for the array comparison is appropriate.


54-95: LGTM!

Comprehensive coverage of all three call types (single, batch, delegatecall) plus the invalid mode error path. The delegate mode test correctly expects this.gateway as the caller since delegatecall executes in the executor's context.


152-156: LGTM!

Good verification that reconfigure is restricted to self-calls only.

contracts/crosschain/CrosschainRemoteExecutor.sol (5)

10-18: LGTM!

State variables are appropriately private with public getters. The CrosschainControllerSet event captures both gateway and controller for proper observability.


20-22: LGTM!

Constructor delegates to _setup, ensuring consistent initialization and validation.


32-46: LGTM!

The access control pattern (msg.sender == address(this)) ensures reconfiguration can only happen through the executor itself, typically via a cross-chain call from the authorized controller. The gateway validation via supportsAttribute is an elegant way to verify ERC-7786 compliance while also rejecting EOAs.


48-54: LGTM!

Proper authorization check requiring both gateway instance match and controller sender match. The equal comparison from Bytes library handles the dynamic bytes comparison correctly.


57-74: Verify behavior with short payloads.

If payload is shorter than 32 bytes, the slice payload[0x00:0x20] will revert with an out-of-bounds error. This may be intentional (malformed messages should fail), but consider whether the revert message is clear enough for debugging cross-chain issues.

contracts/crosschain/CrosschainRemoteController.sol (3)

14-18: LGTM!

The mapping key is bytes to support flexible chain identifiers. The using declarations for utility libraries are appropriate.


20-24: LGTM!

Constructor correctly iterates through provided links with allowOverride=false, ensuring no duplicate chain entries are silently overwritten during initialization.


51-64: LGTM!

The _setLink implementation correctly:

  1. Validates the gateway is ERC-7786 compliant
  2. Derives the chain identifier from the counterpart address
  3. Enforces single-registration semantics unless explicitly overriding
  4. Emits the appropriate event

The pattern of deriving the chain key from the counterpart simplifies the API and prevents mismatches.

Copy link
Collaborator Author

@Amxx Amxx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Questions:

  • Do we want CrosschainRemoteExecutor to access calls from one vs multiple controllers ?
    • Decision was made to leave like this.
  • Is the CrosschainRemoteController really usefull, or should be rather have a Library with a function
    • Would possibly result in loss of clarify/bad code separation
library CrosschainRemoteExecutorUtils {
  function execute(
    address gateway, 
    bytes memory executor, 
    Mode mode, 
    bytes memory executionCalldata
  ) internal returns (bytes32) {
    return IERC7786GatewaySource(gateway).sendMessage(executor, abi.encodePacked(mode, executionCalldata), new bytes[](0));
  }
}

@Amxx Amxx added this to the 5.7 milestone Jan 12, 2026
/// @dev Send crosschain instruction to an arbitrary remote executor via an arbitrary ERC-7786 gateway.
function relayCrosschain(
address gateway,
bytes memory executor,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's your reasoning to allow specifying the gateway explicitly? It would be easy to batch together registerRemoteExecutor followed by relayCrosschain.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking that in some cases you may want to use a custom target. Maybe there are two executor (a main one and a secondary) and you want to target one without reconfiguring. Adding/updating the default executor target is a storage operation that is expensive and that some people may want to skip.

@frangio frangio changed the title Add CrosschainRemoteExecutor and CrosschainRemoteController Add CrosschainRemoteExecutor and GovernorCrosschain Feb 19, 2026
@Amxx Amxx requested review from ernestognw and frangio February 26, 2026 14:42
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a side note, I think this remote executor could allow multichain accounts similar to Hyperlane's Interchain Accounts.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Executor is indeed designed to be the "remote identity" of any "account". Can be used by a governor, but also by a timelock, a multisig, or any account

Comment on lines +90 to +96
if (callType == ERC7579Utils.CALLTYPE_SINGLE) {
executionCalldata.execSingle(execType);
} else if (callType == ERC7579Utils.CALLTYPE_BATCH) {
executionCalldata.execBatch(execType);
} else if (callType == ERC7579Utils.CALLTYPE_DELEGATECALL) {
executionCalldata.execDelegateCall(execType);
} else revert ERC7579Utils.ERC7579UnsupportedCallType(callType);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pattern is shared with AccountERC7579. Maybe we should add a function to the ERC7579Utils:

function exec(bytes32 mode, bytes calldata executionCalldata) internal {
       if (callType == CALLTYPE_SINGLE) {
            executionCalldata.execSingle(execType);
        } else if (callType == CALLTYPE_BATCH) {
            executionCalldata.execBatch(execType);
        } else if (callType == CALLTYPE_DELEGATECALL) {
            executionCalldata.execDelegateCall(execType);
        } else revert ERC7579UnsupportedCallType(callType);
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No strong opinion here. I'm not sure we really need to have this helper, but I also don't see any issues in adding it.

Amxx and others added 2 commits March 3, 2026 21:14
Co-authored-by: Francisco Giordano <fg@frang.io>
Co-authored-by: Ernesto García <ernestognw@gmail.com>
@Amxx Amxx merged commit 3daafbf into OpenZeppelin:master Mar 4, 2026
21 checks passed
@Amxx Amxx deleted the crosschain/remote-executor-controller branch March 4, 2026 21:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ERC7786 RemoteExecutor

3 participants